iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0
自我挑戰組

Concurrency in go 讀書心得系列 第 23

23.Error-propagation

  • 分享至 

  • xImage
  •  

使用併發代碼,特別是分布式系統,在系統中很容易出現問題,而且很難確認發生這種問題的原因。仔細考慮問題是如何通過系統傳播的,以及如何最終呈現給用戶,你會為自己,團隊和用戶減少很多痛苦。
在“錯誤處理”一節中,我們討論了如何從goroutine處理錯誤,但我們沒有花時間討論這些錯誤應該是 什麽樣子,或者錯誤應該如何流經一個龐大而覆雜的系統。讓我們花點時間來討論錯誤傳遞的哲學。
許多開發人員認為錯誤傳遞是不值得關注的,或者,至少不是首先需要關注的。
Go試圖通過強制開發者 在調用堆棧中的每一幀處理錯誤來糾正這種不良做法。
首先讓我們看看錯誤的定義。錯誤何時發生,以及錯誤會提供什麽。 錯誤表明您的系統已進入無法完成用戶明確或隱含請求的操作的狀態。 因此,它需要傳遞一些關鍵信息:

發生了什麽
這是錯誤的一部分,其中包含有關所發生事件的信息,例如“磁盤已滿”,“套接字已關閉”或“憑證過
期”。盡管生成錯誤的內容可能會隱式生成此信息,你可以用一些能夠幫助用戶的上下文來完善它。

何時何處發生
錯誤應始終包含一個完整的堆棧跟蹤,從調用的啟動方式開始,直到實例化錯誤。
此外,錯誤應該包含有關它正在運行的上下文的信息。 例如,在分布式系統中,它應該有一些方法來識別 發生錯誤的機器。當試圖了解系統中發生的情況時,這些信息將具有無法估量的價值。
另外,錯誤應該包含錯誤實例化的機器上的時間,以UTC表示。

有效的信息說明
顯示給用戶的消息應該進行自定義以適合你的系統及其用戶。它只應包含前兩點的簡短和相關信息。 一個 友好的信息是以人為中心的,給出一些關於這個問題的指示,並且應該是關於一行文本。

如何獲取更詳細的錯誤信息
在某個時刻,有人可能想詳細了解發生錯誤時的系統狀態。提供給用戶的錯誤信息應該包含一個ID,該ID 可以與相應的日志交叉引用,該日志顯示錯誤的完整信息:發生錯誤的時間(不是錯誤記錄的時間),堆 棧跟蹤——包括你在代碼中自定義的信息。包含堆棧跟蹤的哈希也是有幫助的,以幫助在bug跟蹤器中匯 總類似的問題。

默認情況下,沒有人工幹預,錯誤所能提供的信息少得可憐。 因此,我們可以認為:在沒有詳細信息的情 況下傳播給用戶任何錯誤的行為都是錯誤的。因為我們可以使用搭建框架的思路來對待錯誤處理。可以將 所有錯誤歸納為兩個類別:

1.Bug
2.已知業務及系統意外(例如,網絡連接斷開,磁盤寫入失敗等)

Bug是你沒有為系統定制的錯誤,或者是“原始”錯誤。有時這是故意的,如果在系統多次叠代時出現的錯誤,盡快不可避免的傳遞給了用戶,但接受到用戶反饋後對提高系統健壯性並不是壞處。有時這是偶然
的。在確定如何傳播錯誤,系統隨著時間的推移如何增長以及最終向用戶展示什麽時,這種區別將證明是
有用的。


先來看一個簡單的例子

package main

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"runtime/debug"
)

type MyError struct {
	Inner      error
	Message    string
	StackTrace string
	Misc       map[string]interface{}
}

func wrapError(err error, messagef string, msgArgs ...interface{}) MyError {
	return MyError{
		Inner:      err, // <1> 在這裡儲存我們正在包裝的錯誤。 如果需要調查發生的事情,我們總是希望能夠查看到最低級別的錯誤。
		Message:    fmt.Sprintf(messagef, msgArgs...),
		StackTrace: string(debug.Stack()),        // <2> 這行代碼記錄了創建錯誤時的堆疊追踪。
		Misc:       make(map[string]interface{}), // <3> 這裡我們創建一個雜項信息儲存字段。可以儲存並發ID,堆疊追踪的hash或可能有助於診斷錯誤的其他上下文信息。
	}
}

func (err MyError) Error() string {
	return err.Message
}

// "lowlevel" module

type LowLevelErr struct {
	error
}

func isGloballyExec(path string) (bool, error) {
	info, err := os.Stat(path)
	if err != nil {
		return false, LowLevelErr{(wrapError(err, err.Error()))} // <1> 在這裡,我們用自定義錯誤來封裝os.Stat中的原始錯誤。在這種情況下,我們不會掩蓋這個錯誤產生的信息。
	}
	return info.Mode().Perm()&0100 == 0100, nil
}

// "intermediate" module

type IntermediateErr struct {
	error
}

func runJob(id string) error {
	const jobBinPath = "/bad/job/binary"
	isExecutable, err := isGloballyExec(jobBinPath)
	if err != nil {
		return err // <1> 我們傳遞來自 lowlevel 模組的錯誤,由於我們接收從其他模組傳遞的錯誤而沒有將它們包裝在我們自己的錯誤類型中,這將會產生問題。
	} else if isExecutable == false {
		return wrapError(nil, "job binary is not executable")
	}

	return exec.Command(jobBinPath, "--id="+id).Run() 
}

func handleError(key int, err error, message string) {
	log.SetPrefix(fmt.Sprintf("[logID: %v]: ", key))
	log.Printf("%#v", err) // <3> 在這裡我們記錄完整的錯誤,以備需要深入了解發生了什麼。
	fmt.Printf("[%v] %v", key, message)
}

func main() {
	log.SetOutput(os.Stdout)
	log.SetFlags(log.Ltime | log.LUTC)

	err := runJob("1")
	if err != nil {
		msg := "There was an unexpected issue; please report this as a bug."
		if _, ok := err.(IntermediateErr); ok { // <1> 在這裡我們檢查是否錯誤是預期的類型。 如果是,可以簡單地將其消息傳遞給用戶。
			msg = err.Error()
		}
		handleError(1, err, msg) // <2> 在這一行中,將日誌和錯誤消息與ID綁定在一起。我們可以很容易增加這個增量,或者使用一個GUID來確保一個唯一的ID。
	}
}
輸出:
[logID: 1]: 14:32:48 main.LowLevelErr{error:main.MyError{Inner:(*fs.PathError)(0x140000721e0), Message:"stat /bad/job/binary: no such file or directory", StackTrace:"goroutine 1 [running]:\nruntime/debug.Stack()\n\t/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:24 +0x64\nmain.wrapError({0x102657148, 0x140000721e0}, {0x14000016180?, 0x60000001000000?}, {0x0?, 0x102894108?, 0x60?})\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation.go:22 +0x60\nmain.isGloballyExec({0x10261611e?, 0x10257963c?})\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation.go:40 +0x5c\nmain.runJob({0x102614584, 0x1})\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation.go:53 +0x34\nmain.main()\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation.go:73 +0x64\n", Misc:map[string]interface {}{}}}
[1] There was an unexpected issue; please report this as a bug.

我們可以看到,在這個錯誤路徑的某處,它沒有正確處理,並且因為我們無法確定錯誤信息是否適合用戶自行處理,所以我們輸出一個簡單的錯誤信息,指出意外事件發生了。
如果回顧 lowlevel 模塊,我們會發現錯誤發生的原因:我們沒有包裝來自 lowlevel 模塊的錯誤。
讓我們糾正它:

func runJob(id string) error {
	const jobBinPath = "/bad/job/binary"
	isExecutable, err := isGloballyExec(jobBinPath)
	if err != nil {
		return IntermediateErr{wrapError(
			err,
			"cannot run job %q: requisite binaries not available",
			id,
		)} // <1>我們現在使用自定義錯誤。我們想隱藏工作未運行原因的底層細節,因為這對於用戶並不重要。
	} else if isExecutable == false {
		return wrapError(
			nil,
			"cannot run job %q: requisite binaries are not executable",
			id,
		)
	}

	return exec.Command(jobBinPath, "--id="+id).Run()
}

當我們重新運行修改後的程式時,會得到錯誤log:

[logID: 1]: 14:36:18 main.IntermediateErr{error:main.MyError{Inner:main.LowLevelErr{error:main.MyError{Inner:(*fs.PathError)(0x140000721e0), Message:"stat /bad/job/binary: no such file or directory", StackTrace:"goroutine 1 [running]:\nruntime/debug.Stack()\n\t/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:24 +0x64\nmain.wrapError({0x104397148, 0x140000721e0}, {0x14000016180?, 0x0?}, {0x0?, 0x6?, 0x0?})\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation-corrected.go:22 +0x60\nmain.isGloballyExec({0x1043562fe?, 0x1042b96dc?})\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation-corrected.go:40 +0x5c\nmain.runJob({0x104354764, 0x1})\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation-corrected.go:53 +0x38\nmain.main()\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation-corrected.go:81 +0x64\n", Misc:map[string]interface {}{}}}, Message:"cannot run job \"1\": requisite binaries not available", StackTrace:"goroutine 1 [running]:\nruntime/debug.Stack()\n\t/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:24 +0x64\nmain.wrapError({0x1043972a8, 0x14000010300}, {0x10435c67b?, 0x10440ba90?}, {0x1400006ee40?, 0x1f0042f4f5c?, 0x14000048768?})\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation-corrected.go:22 +0x60\nmain.runJob({0x104354764, 0x1})\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation-corrected.go:55 +0x188\nmain.main()\n\t/Users/samchen/GolandProjects/concurrency-in-go-src/concurrency-at-scale/error-propagation/fig-error-propagation-corrected.go:81 +0x64\n", Misc:map[string]interface {}{}}}
[1] cannot run job "1": requisite binaries not available

錯誤信息變得清楚:[1] cannot run job "1": requisite binaries not available


修改後的完整程式碼如下:

package main

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"runtime/debug"
)

type MyError struct {
	Inner      error
	Message    string
	StackTrace string
	Misc       map[string]interface{}
}

func wrapError(err error, messagef string, msgArgs ...interface{}) MyError {
	return MyError{
		Inner:      err, //<1>
		Message:    fmt.Sprintf(messagef, msgArgs...),
		StackTrace: string(debug.Stack()),        // <2>
		Misc:       make(map[string]interface{}), // <3>
	}
}

func (err MyError) Error() string {
	return err.Message
}

// "lowlevel" module

type LowLevelErr struct {
	error
}

func isGloballyExec(path string) (bool, error) {
	info, err := os.Stat(path)
	if err != nil {
		return false, LowLevelErr{(wrapError(err, err.Error()))} // <1>
	}
	return info.Mode().Perm()&0100 == 0100, nil
}

// "intermediate" module

type IntermediateErr struct {
	error
}

func runJob(id string) error {
	const jobBinPath = "/bad/job/binary"
	isExecutable, err := isGloballyExec(jobBinPath)
	if err != nil {
		return IntermediateErr{wrapError(
			err,
			"cannot run job %q: requisite binaries not available",
			id,
		)} // <1>
	} else if isExecutable == false {
		return wrapError(
			nil,
			"cannot run job %q: requisite binaries are not executable",
			id,
		)
	}

	return exec.Command(jobBinPath, "--id="+id).Run()
}

func handleError(key int, err error, message string) {
	log.SetPrefix(fmt.Sprintf("[logID: %v]: ", key))
	log.Printf("%#v", err)
	fmt.Printf("[%v] %v", key, message)
}

func main() {
	log.SetOutput(os.Stdout)
	log.SetFlags(log.Ltime | log.LUTC)

	err := runJob("1")
	if err != nil {
		msg := "There was an unexpected issue; please report this as a bug."
		if _, ok := err.(IntermediateErr); ok {
			msg = err.Error()
		}
		handleError(1, err, msg)
	}
}


上一篇
22.Context
下一篇
24.Timeout and Cancellation
系列文
Concurrency in go 讀書心得30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言